/*
 * msgsrv.c
 * 
 * Copyright 2012 Kamil Cukrowski <kamil@dyzio.pl>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 * 
 */

#define MSGSRV_VERSION "0.1.10"
#include <errno.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <syslog.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>

#include "libs/msgserv.h" /* configuration and struct definitions */


#define MY_NAME "msgsrv"
#define BUFF_LEN 256
#define PING_TIME 30


#define PINFO(smt, ...) printf(smt "\n", ##__VA_ARGS__ );
#define PDEBUG(str, ...) PDEBUGL(1, str, ##__VA_ARGS__);
#define PDEBUGL(x, str, ...) do{ if ( debug >= x ) _INTERNAL_LOGIT(str, ##__VA_ARGS__); }while(0)
#define PERROR(str, ...) do { printf("msguhc:%s[%s]%d: ", __FILE__, __FUNCTION__, __LINE__); printf(str, ##__VA_ARGS__); printf("error %d:%s \n", errno, strerror(errno)); }while(0)
#define _INTERNAL_LOGIT(str, ...) do { printf("msguhc:%s[%s]%d: ", __FILE__, __FUNCTION__, __LINE__); printf(str, ##__VA_ARGS__); printf("\n"); }while(0)


#define MSGSRV_BACKLOG  SOMAXCONN-1 /* how many pending connections queue will hold | SOMAXCONN - maximal :D */
#define max(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (_x) > (_y) ? (_x) : (_y); })
#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (_x) > (_y) ? (_y) : (_x); })

struct guest_struct
{
	int type; /* type of guest  - one of MSG_CONNECT_CLI */
	int fd; /* file descriptor for this guest */
	
	struct in_addr addr; /* ip-address of this guest */
	 
	/* pinging */
	int pinging; /* was the guest pinged */
	time_t ping_time; /* when the next pinging will occur */
};

struct guest_struct *guest; /* connections info */
int guests = 0; /* guest number */

struct message_struct *message;
int messages = 0;			//number of messages stored at the server

in_addr_t my_ip = INADDR_ANY;
int sockfd;  /* listen on sock_fd */
int tmp_fd; /* new connection temportary */
int debug = 0;
char buff[BUFF_LEN];


/* --------- safe exit and sig hup and fatal ------------- */

static void safe_exit()
{
	int i;
	for (i=0;i<guests;i++) {
		msglcd_send(guest[i].fd,MSGLCD_MESG_EXIT,3);
	}
	usleep(1000); /* give all guests and guests time to close */
	for (i=0;i<guests;i++) {
		close(guest[i].fd);
	}
	if ( debug == 0 ) {
		closelog();
	}
	close(sockfd);
	close(tmp_fd);
	free(message);
	free(guest);
	exit(0);
}

#define fatal(mesg, ...) \
do {\
	PERROR(mesg, ##__VA_ARGS__);\
	safe_exit();\
} while (0)

static void sig_hup ()
{
	PDEBUG("Cought signal. Shutting down.\n");
	safe_exit();
}

//--------------- misc -------------------------

static void parse_cmd(int argc, char *argv[])
{
	int c=0;
	extern char *optarg;
	while (c != -1) {
		c=getopt(argc, argv, "hdvi:");
		switch (c) {
		case 'h':
			printf("MsgSrv: messaging system server.\n"
				"\t-h\tguest this help.\n"
				"\t-d\tdebug monde on (do not fork into the background);\n"
				"\t-v\tprint version and exit\t"
				"\t-i<IP address>\t server ip address.\n");
			exit(0);
			
		case 'v':
			printf("Version: %s\n", MSGSRV_VERSION);
			exit(0);
		case 'd':
			debug++;
			break;
		case 'i':
			my_ip=inet_addr(optarg);
			break;
		}
	}
}

static void new_message(
	int32_t guest, 
	struct in_addr addr,
	unsigned char nr, 
	unsigned char pri, 
	time_t time)
{
	int i = messages;
	++messages;
	message = realloc(message, sizeof(*message)*messages);
	if (NULL == message) fatal("cannot allocate memory for messages");
	message[i].guest=guest;
	message[i].addr=addr;
	message[i].nr=nr;
	message[i].pri=pri;
	message[i].time=time;
	memset(message[i].mesg, ' ', sizeof(message[i].mesg)/sizeof(*message[i].mesg));
}

static void free_message(int i)
{
	--messages;
	if ( i != messages )
		message[i]=message[messages];
	message = realloc(message, sizeof(*message)*messages);
	if (messages && message == NULL) 
		fatal("cannot free mem for messagse");
}

static void guest_disconnect(unsigned char i)
{
	extern void msg_clean_guest(int guest);
	// i = ( (unsigned long long)guest - (unsigned long long)gu ) / sizeof(*guest);
	if (i >= guests)
		fatal("guest_disconnect: guest does not exits %d %d |", i, guests);
	
	PDEBUG("Disconnecting guest %d connected from %s.\n",guest[i].fd,inet_ntoa(guest[i].addr));
	
	close(guest[i].fd);
	
	if (guest[i].type == MSG_CONNECT_CLI)
		msg_clean_guest(guest[i].fd);
	
	--guests;
	for (; i < guests; i++ ) /* move guests one guest to the left */
		guest[i] = guest[i+1];
	
	guest = realloc(guest, sizeof(*guest)*guests);
	if (guests && NULL == guest) 
		fatal("connaot allocate memoery");
}

//-------------- misc end -------------------
//-----------------------mgs--------------------

static void msg_send_all_disp(char *buff, int len)
{
	int i;
	PDEBUGL(5, "sending ? meessage with len %d \n", len);
	for (i=0; i < guests; i++) {
		if ( guest[i].type == MSG_CONNECT_LCD ) {
			if (msglcd_send(guest[i].fd, buff, len) == -1 ) {
				PDEBUG("Error sendig to LCD %d.",guest[i].fd);
				guest_disconnect(i);
				i--;
			}
			
			/* every time sended, lcds should send #OK back to me */
			if ( !guest[i].pinging ) {
				guest[i].pinging = 1;
				guest[i].ping_time = time(NULL) + PING_TIME;
			}
			
		}
	}
}

static void msg_forward_all_disp(int mesg_nr, char mode )
{
	// mode=1 forward; mode=0 delete
	int len;
	len=3 + sizeof(struct message_struct) - (1-mode)*MSG_LEN;
	buff[0]='#';
	buff[1]=MSG_UP;
	buff[2]='#';
	*(struct message_struct *)(buff+3) = message[mesg_nr];
	msg_send_all_disp( buff, len );
}

static int msg_forward(int fd, int mesg_nr, char mode)
{
	// mode=1 forward; mode=0 delete
	int len;
	len=3 + sizeof(struct message_struct) - (1-mode)*MSG_LEN;
	buff[0]='#';
	buff[1]=MSG_UP;
	buff[2]='#';
	*(struct message_struct *)(buff+3) = message[mesg_nr];
	if ( ( msglcd_send(fd, buff, len ) == -1 ) )
		return(-1);
	return(0);
}

static void msg_dump(FILE *fd)
{
	int i,j;
	char *c;
	struct tm *time_rec;
	time_t tmp_time;
	fprintf(fd,"%d guests connected.\n",guests);
	for (i=0;i<guests;i++) {
		fprintf(fd,"Client #%d: type=%c id=%d, ip=%s\n",
			i,guest[i].type,guest[i].fd,inet_ntoa(guest[i].addr));
	}
	if (messages==0) {
		fprintf(fd,"Nothing to dump.\n");
	} else {
		fprintf(fd,"Dumping messages from %d guests\n",guests);
		fprintf(fd,
			"+---+---+---------------|---+---+-----+----------------------------------------+\n"
			"|msg|cli|  IP address   |nr |pri|time |              text                      |\n"
			"+---+---+---------------|---+---+-----+----------------------------------------+\n");
		for (i=0;i<messages;i++) {
			tmp_time=(time_t) message[i].time;
			time_rec=localtime(&tmp_time);
			fprintf(fd,"|%3d|%3d|",
				i,message[i].guest);
			for (j=15-strlen((c=inet_ntoa(message[i].addr)));j>0;j--)
				fputc(' ',fd);
			fprintf(fd,"%s",c);
			fprintf(fd,"|%3d|%3d|%.2d:%.2d|",
				message[i].nr,message[i].pri,
				(*time_rec).tm_hour,(*time_rec).tm_min);
			for (j=0;j<MSG_LEN;j++) {
				if ( message[i].mesg[j]<8 )
					fputc(message[i].mesg[j]+'0',fd);
				else
					fputc(message[i].mesg[j],fd);
			}
			fprintf(fd,"|\n");
		}
		fprintf(fd,"+---+---+---------------+---+---+-----+----------------------------------------+\n");
	}
}

void msg_clean_guest (int guest)
{
	int i=0;
	PDEBUGL(2, "Cleaning messages from %d.\n", guest);
	if ( guests ) {
		struct message_struct *mesg = (struct message_struct *) (buff+3);
		buff[0]='#';
		buff[1]=MSG_DEL;
		buff[2]='#';
		mesg->guest = guest;
		msg_send_all_disp( buff, 7 );
	}
	while ( i<messages ) {
		if ( message[i].guest == guest ) {
			free_message(i);
		} else {
			i++;
		}
	}
}

static int msg_mng_cli(int guest_nr,char *buff,int len)
{
	int i,j;
	if ( buff[0] != '#' ) return(-1);
	switch (buff[1]) {
	case MSG_UP:
		if ( ( buff[2] != '#' ) || ( buff[4] != '#' ) || ( buff[6] != '#' ) ) {
			PDEBUG("error parsing message.");	
			return(-1);
		}
		i = messages;
		while ( (--i>=0) && ( (message[i].guest != guest[guest_nr].fd) || (message[i].nr!=buff[3] ) ) );
		if ( i == -1 ) {
			PDEBUG("adding mesage %d ", i);
			i = messages;
			new_message(guest[guest_nr].fd, guest[guest_nr].addr, buff[3], buff[5], time(NULL));
		} else if ( len == 7 ) {
			PDEBUG("Deletting one message %d ", i);
			msg_forward_all_disp(i,0);
			free_message(i);
			break;
		} else if ( len-7 > MSG_LEN ) {
			PDEBUG("message too long %d ", i);
			return(-1);
		} else {
			PDEBUGL(2, "updating message %d ", i);
		}
		message[i].time = (uint32_t) time(NULL);
		/* check for any changes in message */
		if ( message[i].pri == buff[5] && !memcmp(message[i].mesg, buff+7, len-7) ) {
			return(0);
		}
		message[i].pri = buff[5];
		memcpy(message[i].mesg, buff+7, len-7);
		if ( len - 7 != MSG_LEN ) {
			for(j = len - 7; j < MSG_LEN; j++)
				message[i].mesg[j] = ' ';
			message[i].mesg[MSG_LEN-1] = 0;
		}
		msg_forward_all_disp(i, 1);
		break;
	case MSG_DEL:
		if ( len != 2 ) {
			PDEBUG("Invalid lenght of 'del' message.");
			return(-1);
		}
		msg_clean_guest(guest[guest_nr].fd);
		break;
	case MSG_LCD:
		if ( buff[2] != '#' ) return(-10);
		if ( buff[3] != '#' ) return(-10);
		if ( buff[4] != '#' ) return(-10);
		PDEBUGL(2, "Queuering message sstraight to lcds");
		msg_send_all_disp(buff, len);
		break;
	default:
		return(-1);
		break;
	}
	return(0);
}

//------------------msg end------------

/* ------------- handle connections ------------- */

static void socket_init()
{
	struct sockaddr_in my_addr;	/* my address information */
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if ( sockfd == -1 )
		fatal("socket");
	fcntl(sockfd, F_SETFL, O_NONBLOCK);
	
	my_addr.sin_family = PF_INET;			/* host byte order */
	my_addr.sin_port = htons(MSGLCD_PORT);	  /* short, network byte order */
	my_addr.sin_addr.s_addr = my_ip; /* auto-fill with my IP */
	bzero(&(my_addr.sin_zero), 8);		  /* zero the rest of the struct */
	
	if ( bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)) == -1 ) 
		fatal("bind");
	if ( listen(sockfd, MSGSRV_BACKLOG) == -1 ) 
		fatal("listen");
}

static int wait_for_data(fd_set *set, const int how_long)
{
	int max_fd,i;
	struct timeval timeout;
	timeout.tv_sec = how_long;
	timeout.tv_usec = 0;
	FD_ZERO(set);
	FD_SET(sockfd,set);
	max_fd=sockfd;
	for (i=0;i<guests;i++) {
		FD_SET(guest[i].fd,set);
		max_fd=max(max_fd,guest[i].fd);
	}
	for (i=0;i<guests;i++) {
		FD_SET(guest[i].fd,set);
		max_fd=max(max_fd,guest[i].fd);
	}
	if (how_long)
		return(select(max_fd+1,set,NULL,NULL,&timeout));
	return(select(max_fd+1,set,NULL,NULL,NULL));
}

/* returns time in secends till next ping event */
static int ping_all_guests(void)
{
	time_t time_now = time(NULL);
	time_t new_wait = time_now + PING_TIME;
	unsigned int i;
	
	for(i=0; i < guests; i++) {
		if ( guest[i].ping_time <= time_now || guest[i].ping_time > time_now+3*PING_TIME ) {
			PDEBUGL(3, "pinging1 guest %d \n", i);
			if (guest[i].pinging == 0 || guest[i].type == MSG_CONNECT_UNSPECIFIED) {
				if ( msglcd_send(guest[i].fd, MSGLCD_MESG_PING, 3) ) 
					fatal("ping 2");
				guest[i].pinging = 1;
				guest[i].ping_time += PING_TIME;
			} else {
				PINFO("guest fd:%d, nr:%d idle too long. disconnecting", guest[i].fd, i);
				guest_disconnect(i);
				continue;
			}
		}
		
		new_wait = min(new_wait, guest[i].ping_time);
		
	}
	new_wait -= time_now;
	return ( new_wait < 0 ? 0 : new_wait );
}

static void connection_init(void)
{
	struct sockaddr_in their_addr; /* connector's address information */
	unsigned int sin_size = sizeof(struct sockaddr_in);
	
	tmp_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
	if (tmp_fd == -1) {
		if (errno == EAGAIN) {
			PERROR("acccept\n");
			return;
		} else {
			fatal("accept");
		}
	}
	PDEBUG("got connection from %s\n", inet_ntoa(their_addr.sin_addr));
	
	++guests;
	guest = realloc(guest, sizeof(*guest)*guests);
	if (NULL == guest) fatal("connot allocate memoery");
	guest[guests-1].type = MSG_CONNECT_UNSPECIFIED;
	guest[guests-1].fd = tmp_fd;
	guest[guests-1].addr = their_addr.sin_addr;
	guest[guests-1].pinging = 0;
	guest[guests-1].ping_time = time(NULL) + PING_TIME;
}

static void connection_init_type(int i, const char *buff) 
{
	struct guest_struct *gu = &guest[i];
	
	PDEBUG(" %s ", __func__);
	
	gu->type = buff[3];
	
	switch ( gu->type ) {
	case MSG_CONNECT_LCD:
		PDEBUG("New display connected as %d from up %s. Forwarding messages.",gu->fd, inet_ntoa(gu->addr));
		if ( msglcd_send(gu->fd, MSGLCD_MESG_OK, 3) == -1 ) {
			PDEBUG("error sending ok message");
			goto error;
		}
		for (i=0;i<messages;i++) {
			if ( msg_forward(gu->fd, i, 1) == -1 ) {
				PDEBUG("Display communication error. Disconnecting guest %d.",gu->fd);
			goto error;
			}
		}
		break;
	case MSG_CONNECT_CLI:
		PDEBUG("New Client connected in fd: %d.\n",gu->fd);
		if ( msglcd_send(gu->fd,MSGLCD_MESG_OK,3) == -1 ) {
			PDEBUG("error sending replay message");
			goto error;
		}
		break;
	default:
		PDEBUG("Parser error while connection init. \n");
		msglcd_send(gu->fd, MSGLCD_MESG_ERROR, 3);
		goto error;
	}
	
	//gu->type = buff[3];
	
	return; /*success*/
error:
	return guest_disconnect(i);
}

static void connection_handle(const fd_set set) 
{
	unsigned int i, j;
	struct guest_struct *gu;
	
	/* which guest sended that message? */
	for (i = 0; i < guests; ++i ) {
		if (FD_ISSET(guest[i].fd, &set)) 
			break;
	}
	if (i == guests) {
		PDEBUG("HEll messaged me ");
		return; /* that wasn't guest */
	}
	
	gu = &guest[i];
	gu->pinging = 0;
	gu->ping_time = time(NULL) + PING_TIME;
	
	/* 
	 * receive message sended to us
	 * */
	j = msglcd_recv(gu->fd, buff, 256);
	PDEBUGL(5, "msglcd_recv: %d.", j);
	switch (j) {
	case 7: /* mesg len for special purposuses */
		if ( gu->type == MSG_CONNECT_UNSPECIFIED ) 
			/* oo! this connector is unspecified and is sending us messages? */
			goto guest_error;
		if ( !strncmp(buff+2, MSGLCD_MESG_OK, 3) )
			return;
		if ( !strncmp(buff+2, MSGLCD_MESG_EXIT, 3) ) {
			PDEBUG("Exit mesg received from the guest %d.", guest[i].fd);
			goto guest_error;
		}
		if ( !strncmp(buff+2, MSGLCD_MESG_PING, 3) ) {
			PDEBUG("received ping message from guest %d. strange ", guest[i].fd);
			goto guest_error;
		}
		PDEBUG("Strange mesg received from the guest %d. closing connection", guest[i].fd);
		goto guest_error;
	case 0:
		PDEBUG("perr shutdowned ? ");
		goto guest_error;
	case -1:
		PERROR("recv internal error");
		goto guest_error;
	case -2:
		PINFO("guest recv:buff size too small. Probably a bigger error");
		goto guest_error;
	case -3:
		PINFO("guest recv:Wrong messege length");
		goto guest_error;
	case -4:
		PINFO("Parity error while receiving from the guest %d.", guest[i].fd);
		goto guest_error;
	case -5:
		PERROR("Display %d closed connection to me.", guest[i].fd);
		goto guest_error;
	default:
		if ( i < 0 ) {
			PDEBUG("Error reading %d while connection init. %d ", i, errno);
		goto guest_error;
		}
	}
	
	/* 
	 * ok, lets see what type is the guest that transmitted us the buff 
	 * */
	switch ( gu->type ) {
	case MSG_CONNECT_UNSPECIFIED:
		/* uch! so he wants to announce himself! */
		connection_init_type(i, buff);
		break;
	case MSG_CONNECT_LCD:
		PINFO("Received strange message from the display %d.", gu->fd);
		goto guest_error;
	case MSG_CONNECT_CLI:
		PDEBUG("AAAAAAAAAAAAAAAAAAAAAAAA %d", guests);
		switch ( msg_mng_cli(i, buff+2, j-4) ) {
		case 0:
			if ( msglcd_send(gu->fd, MSGLCD_MESG_OK, 3) != 0 ) {
				PERROR("Error sending OK replay. \n"
					"Closing connection %d. %d connection still active.\n",
					gu->fd,guests-1);
				goto guest_error;
			}
			break;
		case 1: /* indicaties error like too many mesg or invalid priority */
			if ( msglcd_send(guest[i].fd, MSGLCD_MESG_TOMA, 3) != 0 ) {
				PERROR("Error sending TOMA replay. \n"
					"Closing connection %d. %d connection still active.\n",
					gu->fd, guests-1);
				goto guest_error;
			}
			break;
		default:
			PDEBUG("Error handling mesg from guest %d.", gu->fd);
			goto guest_error;
		}
		break;
		
	default:/* this cant happen */
		goto guest_error;
	}
	
	return; /*(succcs */
guest_error:
	guest_disconnect(i);
}

/* ------------- handle connection end ------------------------ */
/* ------------------------ main!! ------------------------ */

int main(int argc, char *argv[])
{
	int active;
	fd_set set;
	time_t wait_time = 0;

	
	parse_cmd(argc,argv);
	socket_init();
	
	signal(SIGHUP, sig_hup);
	signal(SIGQUIT, sig_hup);
	signal(SIGTERM, sig_hup);
	signal(SIGINT, sig_hup);
	
	PINFO("Startup completed. \n Waiting for connections\n ");
	
	while( ( active = wait_for_data(&set, wait_time) ) > -1 ) {  // main accept() loop
		
		if (active != 0) { /* something happened  */
			PDEBUGL(2, "lala1");
			if ( FD_ISSET(sockfd,&set) ) {
				/** is is a new connection? */
				connection_init();
			} else {
				/* old connection */
				connection_handle(set);
			}
		}
		
		/* every time check pings */
		wait_time = ping_all_guests();
		PDEBUGL(3, "Wait time: %ld ", (unsigned long int)wait_time);
		
		if (debug) 
			msg_dump(stdout);
	}
	
	/* damn we are HERE! something nasty occured 
	 * ex. active == -1 : errno=->Bad file descriptor.
	 * ex. guest disconnected before we created a connection with it
	 * what should i do now? */
	fatal("wait_for_data = %d ", active);
	return(-1);
}

